// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

package http

import (
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/autobrr/autobrr/internal/config"
	"github.com/autobrr/autobrr/internal/logger"
	"github.com/autobrr/autobrr/web"

	"github.com/alexedwards/scs/v2"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/r3labs/sse/v2"
	"github.com/rs/cors"
	"github.com/rs/zerolog"
)

type Server struct {
	log zerolog.Logger
	sse *sse.Server
	db  DatabaseHealth

	buildInfo      buildInfo
	config         *config.AppConfig
	allowedOrigins []string
	sessionManager *scs.SessionManager

	actionService         actionService
	apiService            apikeyService
	authService           authService
	downloadClientService downloadClientService
	filterService         filterService
	feedService           feedService
	indexerService        indexerService
	ircService            ircService
	listService           listService
	notificationService   notificationService
	proxyService          proxyService
	releaseService        releaseService
	updateService         updateService
}

type Deps struct {
	Log logger.Logger
	SSE *sse.Server
	DB  DatabaseHealth

	Config         *config.AppConfig
	SessionManager *scs.SessionManager

	Version string
	Commit  string
	Date    string

	ActionService         actionService
	ApiService            apikeyService
	AuthService           authService
	DownloadClientService downloadClientService
	FilterService         filterService
	FeedService           feedService
	IndexerService        indexerService
	IrcService            ircService
	ListService           listService
	NotificationService   notificationService
	ProxyService          proxyService
	ReleaseService        releaseService
	UpdateService         updateService
}

func NewServer(deps Deps) *Server {
	sessionManager := deps.SessionManager
	sessionManager.Lifetime = 24 * time.Hour * 30
	sessionManager.Cookie.Name = "autobrr_user_session"
	sessionManager.Cookie.Persist = false

	srv := &Server{
		log:    deps.Log.With().Str("module", "http").Logger(),
		config: deps.Config,
		allowedOrigins: []string{
			"*",
			//"http://localhost:3000",
			//"http://localhost:7474",
			//"http://127.0.0.1:3000",
			//"http://127.0.0.1:7474",
		},
		sse: deps.SSE,
		db:  deps.DB,
		buildInfo: buildInfo{
			version: deps.Version,
			commit:  deps.Commit,
			date:    deps.Date,
		},

		sessionManager: sessionManager,

		actionService:         deps.ActionService,
		apiService:            deps.ApiService,
		authService:           deps.AuthService,
		downloadClientService: deps.DownloadClientService,
		filterService:         deps.FilterService,
		feedService:           deps.FeedService,
		indexerService:        deps.IndexerService,
		ircService:            deps.IrcService,
		listService:           deps.ListService,
		notificationService:   deps.NotificationService,
		proxyService:          deps.ProxyService,
		releaseService:        deps.ReleaseService,
		updateService:         deps.UpdateService,
	}

	if deps.Config.Config.CorsAllowedOrigins != "*" {
		srv.allowedOrigins = strings.Split(deps.Config.Config.CorsAllowedOrigins, ",")
	}

	return srv
}

func (s *Server) Open() error {
	addr := fmt.Sprintf("%v:%v", s.config.Config.Host, s.config.Config.Port)

	var err error
	for _, proto := range []string{"tcp", "tcp4", "tcp6"} {
		if err = s.tryToServe(addr, proto); err == nil {
			break
		}

		s.log.Error().Err(err).Msgf("Failed to start %s server. Attempted to listen on %s", proto, addr)
	}

	return err
}

func (s *Server) tryToServe(addr, protocol string) error {
	listener, err := net.Listen(protocol, addr)
	if err != nil {
		return err
	}

	s.log.Info().Msgf("Starting API %s server. Listening on %s", protocol, listener.Addr().String())

	server := http.Server{
		Handler:           s.Handler(),
		ReadHeaderTimeout: time.Second * 15,
	}

	return server.Serve(listener)
}

func (s *Server) Handler() http.Handler {
	r := chi.NewRouter()

	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(middleware.Recoverer)
	r.Use(LoggerMiddleware(&s.log))

	c := cors.New(cors.Options{
		AllowCredentials:   true,
		AllowedMethods:     []string{http.MethodHead, http.MethodOptions, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete},
		AllowedHeaders:     []string{"Authorization"},
		AllowedOrigins:     s.allowedOrigins,
		OptionsPassthrough: true,
		// Enable Debugging for testing, consider disabling in production
		Debug: false,
	})

	r.Use(c.Handler)
	r.Use(s.sessionManager.LoadAndSave)

	encoder := newEncoder(s.log)

	// Create a separate router for API
	apiRouter := chi.NewRouter()
	apiRouter.Route("/auth", newAuthHandler(encoder, s.log, s, s.config.Config, s.sessionManager, s.authService).Routes)
	apiRouter.Route("/healthz", newHealthHandler(encoder, s.db).Routes)
	apiRouter.Group(func(r chi.Router) {
		r.Group(func(r chi.Router) {
			r.Use(s.IsAuthenticated)

			r.Route("/actions", newActionHandler(encoder, s.actionService).Routes)
			r.Route("/config", newConfigHandler(encoder, s.buildInfo, s.config).Routes)
			r.Route("/download_clients", newDownloadClientHandler(encoder, s.downloadClientService).Routes)
			r.Route("/filters", newFilterHandler(encoder, s.filterService).Routes)
			r.Route("/feeds", newFeedHandler(encoder, s.feedService).Routes)
			r.Route("/irc", newIrcHandler(encoder, s.sse, s.ircService).Routes)
			r.Route("/indexer", newIndexerHandler(encoder, s.indexerService, s.ircService).Routes)
			r.Route("/lists", newListHandler(encoder, s.listService).Routes)
			r.Route("/keys", newAPIKeyHandler(encoder, s.apiService).Routes)
			r.Route("/logs", newLogsHandler(s.config).Routes)
			r.Route("/notification", newNotificationHandler(encoder, s.notificationService).Routes)
			r.Route("/proxy", newProxyHandler(encoder, s.proxyService).Routes)
			r.Route("/release", newReleaseHandler(encoder, s.releaseService).Routes)
			r.Route("/updates", newUpdateHandler(encoder, s.updateService).Routes)
			r.Route("/webhook", newWebhookHandler(encoder, s.listService).Routes)

			r.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {

				// inject CORS headers to bypass checks
				s.sse.Headers = map[string]string{
					"Content-Type":      "text/event-stream",
					"Cache-Control":     "no-cache",
					"Connection":        "keep-alive",
					"X-Accel-Buffering": "no",
				}

				s.sse.ServeHTTP(w, r)
			})
		})
	})

	routeBaseURL := "/"

	webRouter := chi.NewRouter()

	// handle backwards compatibility for base url routing
	if s.config.Config.BaseURLModeLegacy {
		// this is required to keep assets "url rewritable" via a reverse-proxy
		routeAssetBaseURL := "./"
		// serve the web
		webHandlers := newWebLegacyHandler(s.log, web.DistDirFS, s.buildInfo.version, s.config.Config.BaseURL, routeAssetBaseURL)
		webHandlers.RegisterRoutes(webRouter)
	} else {
		routeBaseURL = s.config.Config.BaseURL

		// serve the web
		webHandlers := newWebHandler(s.log, web.DistDirFS, s.buildInfo.version, routeBaseURL, routeBaseURL)
		webHandlers.RegisterRoutes(webRouter)

		// add fallback routes when base url is set to inform user to redirect and use /baseurl/
		r.Get("/", func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusNotFound)
			if err := webHandlers.RenderFallbackIndex(w); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})

		r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusNotFound)
			if err := webHandlers.RenderFallbackIndex(w); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		})
	}

	// Mount the web router under baseUrl + '/'
	r.Mount(routeBaseURL, webRouter)
	// Mount the API router under baseUrl + '/api'
	r.Mount(routeBaseURL+"api", apiRouter)

	return r
}
